package com.icehelper.hades.base.util;


import java.io.IOException;
import java.net.ConnectException;
import java.net.InetAddress;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import com.google.common.base.Charsets;
import com.icehelper.hades.base.log.HadesLogger;
import com.icehelper.hades.base.log.LogConstant;
import com.icehelper.hades.base.log.LogUtil;
import com.icehelper.hades.base.log.SpanIdGenerator;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.StatusLine;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpClient;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.impl.DefaultConnectionReuseStrategy;
import org.apache.http.impl.NoConnectionReuseStrategy;
import org.apache.http.impl.client.DefaultConnectionKeepAliveStrategy;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;

/**
 * Http Client 工具类。使用时先用client()方法或client(options)方法获取实例。
 */
public class HttpClientHelper {

  private static final HadesLogger logger = HadesLogger.create();

  private HttpOptions options;

  public static HttpClientHelper create() {
    return create(new HttpOptions());
  }

  public static HttpClientHelper create(HttpOptions options) {
    return new HttpClientHelper(options);
  }

  public static HttpClientHelper create(int connectionTimeout, int soTimeout) {
    return create(new HttpOptions(soTimeout, connectionTimeout));
  }

  private HttpClientHelper(HttpOptions options) {
    this.options = options;
  }

  /**
   * No retry handler mechanism
   */
  private final HttpRequestRetryHandler NO_RETRIES_HANDLER =
      (exception, executionCount, context) -> false;
  /**
   * 重试策略
   */
  private final HttpRequestRetryHandler RETRIES_HANDLER =
      new HttpRequestRetryHandler() {
        @Override
        public boolean retryRequest(IOException reqError, int executionCount, HttpContext context) {
          if (executionCount > options.getRetryTimes()) {
            return false;
          }

          boolean match = options.getRetryErrors().stream().anyMatch(ex -> ex.isInstance(reqError));
          if (match) {
            logger.debug("http request retry , executionCount={}", executionCount);
          }
          return match;
        }
      };

  private static void setHeaderKV(HttpUriRequest httpUriRequest, final String childSpanId) {
    if (httpUriRequest == null) {
      return;
    }
    String traceId = LogUtil.getTraceId();
    if (StringUtils.isBlank(traceId)) {
      traceId = LogUtil.genTraceId();
    }
    httpUriRequest.setHeader( LogConstant.KEY_TRACE_ID, traceId);
    httpUriRequest.setHeader( LogConstant.KEY_SPAN_ID, childSpanId);
  }

  public static void httpTraceLog(long beginTime, String childSpanId, String urlPath,
      Throwable exception, boolean success, Object params, int statusCode, Object result,
      String serviceName) {
    try {
      String paramStr = toJsonString(params);
      String resultStr = toJsonString(result);
      final long endTime = System.currentTimeMillis();

      MapBuilder<String, String> kvBuilder =
          MapBuilder.create("service", "Http-Req")
              .add("interface", urlPath)
              .add("url", urlPath)
              .add("uri", LogUtil.getUri())
              .add("success", Objects.toString(success))
              .add("costTime", Objects.toString(endTime - beginTime))
              .add("proc_time", Objects.toString(endTime - beginTime))
              .add("clientIp", localIp)
              .add("timestamp", Objects.toString(endTime))
              .add("statusCode", Objects.toString(statusCode))
              .add("childSpanId", childSpanId)
              .add(LogConstant.KEY_CHILD_SPAN_ID, childSpanId)
              .add("service_name", serviceName);

      final int maxLength = 4096;
      if (Inspections.isNotBlank(paramStr)) {
        kvBuilder.add("rpcMethodArgs", StringUtils.substring(paramStr, 0, maxLength));
      }
      if (Inspections.isNotBlank(resultStr)) {
        kvBuilder.add("rpcMethodResult", StringUtils.substring(resultStr, 0, maxLength));
      }

      if (success) {
        LogUtil.addKVs(kvBuilder.build());
        LogUtil.setDlTag(LogConstant.DLTAG_COM_HTTP_SUCCESS);
        logger.addKvs(kvBuilder.build())
            .dlInfo(LogConstant.DLTAG_COM_HTTP_SUCCESS, "http call succeeded");
      } else {
        String dlTag = null;
        dlTag = LogConstant.DLTAG_COM_HTTP_FAILURE;
        if (exception instanceof SocketTimeoutException) {
          dlTag = "_com_http_SocketTimeoutException";
        } else if (exception instanceof ConnectTimeoutException) {
          dlTag = "_com_http_ConnectTimeoutException";
        } else if (exception instanceof ConnectException) { // typically connect refused
          dlTag = "_com_http_ConnectException";
        }
        kvBuilder.add("exception", Objects.toString(exception));

        LogUtil.addKVs(kvBuilder.build());
        LogUtil.setDlTag(LogConstant.DLTAG_COM_HTTP_SUCCESS);
        logger.addKvs(kvBuilder.build())
            .dlInfo(LogConstant.DLTAG_COM_HTTP_FAILURE, "http call succeeded");
      }
    } catch (Throwable t) {
      logger.error("Error while trace log for http. ", t);
    }
  }

  private static String toJsonString(Object obj) {
    if (obj == null) {
      return "null";
    } else if (obj instanceof String) {
      return obj.toString();
    } else {
      return JsonHelper.toJson(obj);
    }
  }

  /**
   * process get request auto check if is http or https
   */
  public String doGet(String urlPath) {
    return doGet(urlPath, Charsets.UTF_8, null, null);
  }

  /**
   * @param urlPath
   * @param charset
   * @param params
   * @param headers
   * @return
   */
  public String doGet(
      String urlPath, Charset charset, Map<String, String> params, Map<String, String> headers) {
    Throwable failureException = null;
    boolean success = true;
    String response = null;
    long beginTime = System.currentTimeMillis();
    String childSpanId = SpanIdGenerator.generatorSpanId();
    int statusCode = -1;
    try {
      URIBuilder uriBuilder = new URIBuilder(urlPath);
      if (params != null && !params.isEmpty()) {
        for (Map.Entry<String, String> e : params.entrySet()) {
          uriBuilder.addParameter(e.getKey(), e.getValue());
        }
      }
      HttpGet httpGet = new HttpGet(uriBuilder.build());
      if (headers != null && !headers.isEmpty()) {
        for (Map.Entry<String, String> e : headers.entrySet()) {
          httpGet.addHeader(e.getKey(), e.getValue());
        }
      }

      HttpClient client = buildClient(urlPath, null);
      setHeaderKV(httpGet, childSpanId);

      HttpResponse httpResponse = client.execute(httpGet);
      ResponseStatus responseStatus = handleResponse(httpResponse, charset);
      statusCode = responseStatus.statusCode;
      success = (statusCode >= 200 && statusCode < 300);
      response = responseStatus.response;
    } catch (IOException e) {
      failureException = e;
      success = false;
    } catch (Exception e) {
      failureException = e;
      success = false;
    } finally {
      String serviceName = options.getServiceName();
      httpTraceLog(
          beginTime, childSpanId, urlPath, failureException, success, params, statusCode, response,
          serviceName);
    }
    return response;
  }

  /**
   * Submit post request, auto check if is http or https
   */
  public String doPost(String urlPath, String content) {
    return doPost(urlPath, content, Charsets.UTF_8, null);
  }

  /**
   * Submit post request with specified encoding, auto check if is http or https
   */
  public String doPost(
      String urlPath, String content, Charset charset, Map<String, String> headers) {
    HttpClient client = buildClient(urlPath, null);
    return doPost(client, urlPath, content, charset, headers);
  }

  /**
   * Submit http post request and put content in body directly
   */
  private String doPost(
      HttpClient client,
      String urlPath,
      String content,
      Charset charset,
      Map<String, String> headers) {
    Throwable failureException = null;
    boolean success = true;
    String response = null;
    long beginTime = System.currentTimeMillis();
    String childSpanId = SpanIdGenerator.generatorSpanId();
    int statusCode = -1;
    try {
      HttpPost post = new HttpPost(urlPath);
      if (headers != null && !headers.isEmpty()) {
        for (Map.Entry<String, String> e : headers.entrySet()) {
          post.addHeader(e.getKey(), e.getValue());
        }
      }

      setHeaderKV(post, childSpanId);
      post.setEntity(new ByteArrayEntity(content.getBytes(charset)));

      HttpResponse httpResponse = client.execute(post);
      ResponseStatus responseStatus = handleResponse(httpResponse, charset);
      statusCode = responseStatus.statusCode;
      success = (statusCode >= 200 && statusCode < 300);
      response = responseStatus.response;
    } catch (IOException e) {
      failureException = e;
      success = false;
    } catch (Exception e) {
      failureException = e;
      success = false;
    } finally {
      String serviceName = options.getServiceName();
      httpTraceLog(
          beginTime,
          childSpanId,
          urlPath,
          failureException,
          success,
          content,
          statusCode,
          response, serviceName);
    }
    return response;
  }

  /**
   * Submit post request, default UTF-8, auto check if is http or https
   */
  public String doPost(String urlPath, Map<String, String> params) {
    return doPost(urlPath, params, Charsets.UTF_8, null);
  }

  /**
   * Submit post request with specified encoding, auto check if is http or https
   */
  public String doPost(
      String urlPath, Map<String, String> params, Charset charset, Map<String, String> headers) {
    HttpClient client = buildClient(urlPath, null);
    return doPost(client, urlPath, params, charset, headers);
  }

  /**
   * Submit one-way https post request
   */
  public String doPostWithSSL(String urlPath, Map<String, String> params) {
    HttpClient client = buildClient(urlPath, null);
    return doPost(client, urlPath, params, Charsets.UTF_8, null);
  }


  /**
   * POST上传文件
   */
  public String doPostWithFile(String urlPath,
      Map<String, String> params, byte[] file, String fileName,Charset charset, Map<String, String> headers) {

    HttpClient client = buildClient(urlPath, null);
    return doPostWithFile(client, urlPath, params, file, fileName,charset, headers);
  }

  /**
   * POST上传文件
   */
  public String doPostWithFile(HttpClient client, String urlPath,
      Map<String, String> params, byte[] file, String fileName,Charset charset,
      Map<String, String> headers) {
    Throwable failureException = null;
    boolean success = true;
    String response = null;
    long beginTime = System.currentTimeMillis();
    String childSpanId = SpanIdGenerator.generatorSpanId();
    int statusCode = -1;
    try {

      HttpPost post = new HttpPost(urlPath);
      if (headers != null && !headers.isEmpty()) {
        for (Map.Entry<String, String> e : headers.entrySet()) {
          post.addHeader(e.getKey(), e.getValue());
        }
      }

      MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create();
      multipartEntityBuilder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);

      if (params != null && !params.isEmpty()) {
        for (Map.Entry<String, String> e : params.entrySet()) {
          StringBody stringBody = new StringBody(e.getValue(), ContentType.TEXT_PLAIN);
          multipartEntityBuilder.addPart(e.getKey(), stringBody);
        }
      }

      multipartEntityBuilder.addBinaryBody("filecontent",file,ContentType.DEFAULT_BINARY,fileName);

      post.setEntity(multipartEntityBuilder.build());

      setHeaderKV(post, childSpanId);

      HttpResponse httpResponse = client.execute(post);
      ResponseStatus responseStatus = handleResponse(httpResponse, charset);
      statusCode = responseStatus.statusCode;
      success = (statusCode >= 200 && statusCode < 300);
      response = responseStatus.response;
    } catch (IOException e) {
      failureException = e;
      success = false;
    } catch (Exception e) {
      failureException = e;
      success = false;
    } finally {
      String serviceName = options.getServiceName();
      httpTraceLog(
          beginTime, childSpanId, urlPath, failureException, success, params, statusCode, response,
          serviceName);
    }
    return response;
  }


    /**
     * Submit post request with key/value map, specified encoding, headers
     */
    public HttpEntity doPostForEntity(
            HttpClient client,
            String urlPath,
            Map<String, String> params,
            Charset charset,
            Map<String, String> headers) {
        Throwable failureException = null;
        boolean success = true;
        HttpEntity response = null;
        long beginTime = System.currentTimeMillis();
        String childSpanId = SpanIdGenerator.generatorSpanId();
        int statusCode = -1;
        try {

            HttpPost post = new HttpPost(urlPath);
            if (headers != null && !headers.isEmpty()) {
                for (Map.Entry<String, String> e : headers.entrySet()) {
                    post.addHeader(e.getKey(), e.getValue());
                }
            }
            if (params != null && !params.isEmpty()) {
                List<NameValuePair> formParams = new ArrayList<>();
                for (Map.Entry<String, String> e : params.entrySet()) {
                    formParams.add(new BasicNameValuePair(e.getKey(), e.getValue()));
                }
                post.setEntity(new UrlEncodedFormEntity(formParams, charset));
            }

            setHeaderKV(post, childSpanId);

            HttpResponse httpResponse = client.execute(post);
            HttpEntity entity = httpResponse.getEntity();

            try {
                StatusLine statusLine = httpResponse.getStatusLine();
                statusCode = statusLine.getStatusCode();
                success = (statusCode >= 200 && statusCode < 300);
                response = entity == null ? null : entity;
            } finally {
                EntityUtils.consume(entity);
            }
        } catch (IOException e) {
            failureException = e;
            success = false;
        } catch (Exception e) {
            failureException = e;
            success = false;
        } finally {
            String serviceName = options.getServiceName();
            httpTraceLog(
                    beginTime, childSpanId, urlPath, failureException, success, params, statusCode, response,
                    serviceName);
        }
        return response;
    }




  /**
   * Submit post request with key/value map, specified encoding, headers
   */
  public String doPost(
      HttpClient client,
      String urlPath,
      Map<String, String> params,
      Charset charset,
      Map<String, String> headers) {
    Throwable failureException = null;
    boolean success = true;
    String response = null;
    long beginTime = System.currentTimeMillis();
    String childSpanId = SpanIdGenerator.generatorSpanId();
    int statusCode = -1;
    try {

      HttpPost post = new HttpPost(urlPath);
      if (headers != null && !headers.isEmpty()) {
        for (Map.Entry<String, String> e : headers.entrySet()) {
          post.addHeader(e.getKey(), e.getValue());
        }
      }
      if (params != null && !params.isEmpty()) {
        List<NameValuePair> formParams = new ArrayList<>();
        for (Map.Entry<String, String> e : params.entrySet()) {
          formParams.add(new BasicNameValuePair(e.getKey(), e.getValue()));
        }
        post.setEntity(new UrlEncodedFormEntity(formParams, charset));
      }

      setHeaderKV(post, childSpanId);

      HttpResponse httpResponse = client.execute(post);
      ResponseStatus responseStatus = handleResponse(httpResponse, charset);
      statusCode = responseStatus.statusCode;
      success = (statusCode >= 200 && statusCode < 300);
      response = responseStatus.response;
    } catch (IOException e) {
      failureException = e;
      success = false;
    } catch (Exception e) {
      failureException = e;
      success = false;
    } finally {
      String serviceName = options.getServiceName();
      httpTraceLog(
          beginTime, childSpanId, urlPath, failureException, success, params, statusCode, response,
          serviceName);
    }
    return response;
  }

  /**
   * Build http client with authentication mechanism
   */
  private HttpClient buildClient(String url, CredentialsProvider credentialsProvider) {

    HttpClientBuilder builder = null;
    try {
      builder = HttpClientBuilder.create().setConnectionManager(options.connectionManager());
    } catch (Exception e) {
      e.printStackTrace();
    }

    // Set UA
    if (Inspections.isNotBlank(options.getUserAgent())) {
      builder.setUserAgent(options.getUserAgent());
    }

    // Set request params
    builder.setDefaultRequestConfig(options.requestConfig());

    // Set proxy
    if (options.isUseProxy()) {
      builder.setProxy(new HttpHost(options.getProxyHost(), options.getProxyPort()));
    }

    // Set connection reuse and keep alive
    final long keepAlive = options.getKeepAlive();
    if (keepAlive == 0) {
      builder.setConnectionReuseStrategy(new NoConnectionReuseStrategy());
    } else {
      builder.setConnectionReuseStrategy(new DefaultConnectionReuseStrategy());
      builder.setKeepAliveStrategy(
          new DefaultConnectionKeepAliveStrategy() {
            @Override
            public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
              final long duration = super.getKeepAliveDuration(response, context);
              return (duration == -1) ? keepAlive : duration;
            }
          });
    }

    // Retry mechanism
    if (options.getRetryTimes() > 0) {
      builder.setRetryHandler(RETRIES_HANDLER);
    } else {
      builder.setRetryHandler(NO_RETRIES_HANDLER);
    }

    // Authentication mechanism
    if (credentialsProvider != null) {
      builder.setDefaultCredentialsProvider(credentialsProvider);
    }

    // set ssl factory
    URI uri = URI.create(url);
    if ("https".equals(uri.getScheme())) {
      try {
        // Trust all
//        TrustStrategy ts = (chain, authType) -> true;
//        SSLContext sslCxt = new SSLContextBuilder().loadTrustMaterial(null, ts).build();
//        builder.setSSLSocketFactory(new SSLConnectionSocketFactory(sslCxt,NoopHostnameVerifier.INSTANCE));
      } catch (Exception e) {
        logger.warn("build SSLContext failed!", e);
      }
    }

    return builder.build();
  }

  private ResponseStatus handleResponse(HttpResponse response, Charset charset)
      throws HttpResponseException, IOException {
    final StatusLine statusLine = response.getStatusLine();
    final HttpEntity entity = response.getEntity();
    ResponseStatus responseStatus = new ResponseStatus();
    try {
      responseStatus.statusCode = statusLine.getStatusCode();
      responseStatus.response = (entity == null ? null : EntityUtils.toString(entity, charset));
    } finally {
      EntityUtils.consume(entity);
    }
    return responseStatus;
  }

  private class EncodingResponseHandler implements ResponseHandler<String> {

    private Charset charset;

    public EncodingResponseHandler(Charset charset) {
      this.charset = charset;
    }

    /**
     * Returns the response body as a String if the response was successful (a 2xx status code). If
     * no response body exists, this returns null. If the response was unsuccessful (>= 300 status
     * code), throws an {@link org.apache.http.client.HttpResponseException}.
     */
    @Override
    public String handleResponse(final HttpResponse response)
        throws HttpResponseException, IOException {
      final StatusLine statusLine = response.getStatusLine();
      final HttpEntity entity = response.getEntity();
      if (statusLine.getStatusCode() >= 300) {
        EntityUtils.consume(entity);
        throw new HttpResponseException(statusLine.getStatusCode(), statusLine.getReasonPhrase());
      }
      return entity == null ? null : EntityUtils.toString(entity, charset);
    }
  }

  private class ResponseStatus {

    String response;
    int statusCode;
  }

  /**
   * rewrite org.apache.http.client.HttpResponseException
   */
  private class HttpResponseException extends ClientProtocolException {

    private static final long serialVersionUID = -7186627969477257933L;

    private final int statusCode;

    public HttpResponseException(final int statusCode, final String s) {
      super("Status code is " + statusCode + ". " + s);
      this.statusCode = statusCode;
    }

    public int getStatusCode() {
      return this.statusCode;
    }
  }

  private static String localIp = localIp();

  private static String localIp() {
    try {
      InetAddress localAddress = InetAddress.getLocalHost();
      return localAddress.getHostName();
    } catch (Throwable e) {
      logger.warn("Error while get local address", e);
    }
    return null;
  }
}
